当 WS_POPUP 遇上 Win+D:为什么置顶窗口会在 “显示桌面” 时离奇失踪?
近日我在开发一款美化软件,以实现覆盖 StartIsBack / StartAllBack 的功能。目前已完成低级钩子的注入与任务栏的子类化。仓库如下:
https://github.com/Bruce225/anabiosis
开发途中遇到一个我自昨日下午起开始着手解决的 Bug; 在历经数小时令人烦躁的修改与调试后,终得以于深更半夜修复。下简要分享思路与调试过程。
前情提要
想要在 Windows 11 上实现类似 StartAllBack 的开始按钮与开始菜单修改,大多数人的第一想法应该是在获取开始按钮的句柄 hStartBtn 后,使用 ShowWindow(hStartBtn, SW_HIDE) 语句隐藏它,并在任务栏中 “嵌入” 自己的开始按钮。
然而 Windows 11 将许多的任务栏窗口组件使用了 XAML 重绘。即使在 Spy++ 等软件上能够看到 Shell_TrayWnd 下存在 "Start" 类,但他已实质上变成不受属性控制的 “代理句柄” ,导致操作 hStartBtn 对开始按钮不构成任何影响。故开发时,选择「覆盖」而不是「隐藏并添加」是更好的思路。关于「覆盖」部分,我的代码如下:
1 | HWND CreateOrbWindow(HINSTANCE hInstance) |
这相当于在原开始按钮的位置上覆盖了一个自定义 Orb 按钮。其中,指定 hTaskbar 为其父窗口是为了防止点按任务栏时自定义按钮消失。
Bug 的发现
如上的逻辑在 90% 的测试情况下都没有任何问题。快速点按开始按钮、不间断敲打 Win 键或在各种情形下点按任务栏。然而当点击任务栏最右侧 “显示桌面” 按钮,或按下 Win+D 组合键时,该 Orb 按钮会突然消失;再次单击 “显示桌面” ,当左键按下时 (WM_LBUTTONDOWN) 该 Orb 按钮重新显现,松开时 (WM_LBUTTONUP) Orb 按钮仍然消失。若继续点击 “显示桌面” ,上述过程循环往复。
我起初的解决方案是在该 Orb 按钮窗口的过程中,判定 uMsg 时加入如下定时刷新器:
1 | LRESULT CALLBACK OrbWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) |
当快速点按 “显示桌面” 时,虽然 Orb 按钮不断重绘并显现,但仍然会在极短的间隙内闪过原版开始按钮,即使将设定的 50ms 减小也无济于事。况且,该种 Timer 轮询的方法极为消耗 CPU 性能。
调试过程
使用定时器显然不是一个好方法,于是我将他移除了。我试图通过将 CreateWindowExW 中的 WS_POPUP 改为 WS_CHILD 来防止 Orb 按钮随 hTaskbar 一起隐藏,又试图通过拦截 Win+D 可能在执行 “显示桌面” 逻辑时向我的 Orb 按钮窗口发送的隐藏信号。
这两个方法都没有解决 Bug.
偶然间,我关注到了 CreateWindowExW 中的 r.left, r.top, w, h 这些坐标与大小参数,于是将 r.left 与 r.top 修改为了 (0, 0) 以及 (0, 1000) 分别进行调试 (我使用的是 1920 x 1080 的显示器)。最终发现,当按下 “显示桌面” 时,我的 Orb 按钮完全不会被它隐藏!
尤其是 (0, 1000), 我注意到按下 “显示桌面” 时它位于任务栏上方的部分未被隐藏,本身覆盖在开始按钮的那部分消失了。也就是说,这个 Bug 本质上是一个 Z-Order 排列竞争的问题。
我猜测是如下的逻辑:在 Windows 11 中,所有置顶窗口 (WM_EX_TOPMOST 属性) 都在同一个平面的 Z 轴序列里。由于任务栏是系统的核心组件,点击 “显示桌面” 时,Explorer.exe 会为了确保任务栏不被其他窗口 (比如可能存在的全屏置顶广告或工具栏) 遮挡,强制刷新一次自己的 Z 轴排序。于是,刚好把我的 Orb 按钮压在了下面。
彻底修复
我的思路是检测按下 “显示桌面” 后窗口的位置变化 (WM_WINDOWPOSCHANGED). 原本的想法是监控开始按钮 (因为我本身代码中对任务栏进行过子类化并寻过开始菜单的句柄 hStartBtn)。 事实上,通过向 DbgView 输出调试字符串的验证方式后,我发现此时开始按钮并没有发生 WM_WINDOWPOSCHANGED. 因此需要转向检测任务栏的位置变化。
首先是在 dll 劫持函数中将劫持目标选为任务栏类 Shell_TrayWnd, 对其进行子类化,并让其处理函数变为我们编写的 NewTaskbarProc. 代码如下:
1 | void StartHijack(HMODULE hModule) |
其中 hModule 与专用线程相关,与本文内容无关。接着在新逻辑 NewTaskbarProc 函数中,拦截任务栏的位置变化消息 WM_WINDOWPOSCHANGED 并通过 SetWindowPos 函数将自定义 Orb 提前。代码如下:
1 | LRESULT CALLBACK NewTaskbarProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) |
这样,每次点按 “显示桌面” 或 Win+D 时,我们的任务栏行为都能使得自定义 Orb 窗口被提到顶层,中间没有任何间隙 (或极小,肉眼无法察觉), 达到目的。由此 Bug 修复。